package no.uis.archiva.consumer.liferay;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.apache.maven.archiva.common.utils.VersionComparator;
import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration;
import org.apache.maven.archiva.consumers.AbstractMonitoredConsumer;
import org.apache.maven.archiva.consumers.ConsumerException;
import org.apache.maven.archiva.consumers.KnownRepositoryContentConsumer;
import org.apache.maven.artifact.repository.metadata.Metadata;
import org.apache.maven.artifact.repository.metadata.Versioning;
import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.ReaderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import edu.emory.mathcs.backport.java.util.Collections;

/**
 * This consumer scans all meta files and extracts the portlet information from the WAR 
 * corresponding to the latest version.
 *  
 * @author Goldhahn
 * @author Jon Ramvi
 * @plexus.component role="org.apache.maven.archiva.consumers.KnownRepositoryContentConsumer" role-hint="create-liferay-repository"
 *                   instantiation-strategy="keep-alive"
 */
public class LiferayRepositoryCreator extends AbstractMonitoredConsumer implements KnownRepositoryContentConsumer, Initializable {

  /**
   * @plexus.configuration default-value="create-liferay-repository"
   */
  private String id;

  /**
   * @plexus.configuration default-value="Create a Liferay repository descriptor from Maven Artifacts."
   */
  private String description;

  private List<String> includes = new LinkedList<String>();
  
  //private ManagedRepositoryConfiguration repoConfig;
  
  private Logger log = LoggerFactory.getLogger(LiferayRepositoryCreator.class);

  private String liferayVersion;
  
  private String repositoryXmlFileName;

  private AtomicLong processingThreadId = new AtomicLong(-1);
  
  private Map<String, RepoContext> contextMap = new HashMap<String, RepoContext>();

  private ThreadLocal<RepoContext> currentRepoContext = new ThreadLocal<RepoContext>();
  
  private static class RepoContext {
    private File repositoryPath;
    private ManagedRepositoryConfiguration repoConfig;
    private Document packageDom;
    private boolean modified;
  }

  private RepoContext getCurrentRepoContext() {
    return currentRepoContext.get();
  }
  
  private RepoContext getRepoContext(String repoPath) {
    RepoContext repoCtx = contextMap.get(repoPath);
    if (repoCtx == null) {
      repoCtx = new RepoContext();
      repoCtx.repositoryPath = new File(repoPath);
      contextMap.put(repoPath, repoCtx);
    }
    currentRepoContext.set(repoCtx);
    return repoCtx;
  }
  
  private void resetCurrentRepoContext() {
    currentRepoContext.remove();
  }
  
  
  @Override
  public void beginScan(ManagedRepositoryConfiguration repository, Date whenGathered)
      throws ConsumerException
  {
    beginScan(repository, whenGathered, true);
  }

  @Override
  public void beginScan(ManagedRepositoryConfiguration config, Date whenGathered, boolean executeOnEntireRepo) throws ConsumerException {
    
    if (!canStartProcessing()) {
      return;
    }
    RepoContext ctx = getRepoContext(config.getLocation());
    ctx.repoConfig = config;
    ctx.modified = false;
    
    if (log.isInfoEnabled()) {
      log.info("Config location: " + config.getLocation());
    }
    readInitialDom(ctx.repositoryPath);
  }

  @Override
  public void processFile(String fileName, boolean executeOnEntireRepo) throws Exception {

    RepoContext ctx = getCurrentRepoContext();
    if (!isProcessing()) {
      return;
    }
    
    File file = new File(ctx.repositoryPath, fileName);
    if (log.isInfoEnabled()) {
      log.info("Found maven-metadata.xml in " + file.getPath());
    }

    if (!file.canRead()) {
      log.warn("file " + file.getPath() + " cannot be read");
      return;
    }
    MetadataXpp3Reader mappingReader = new MetadataXpp3Reader();

    Metadata metadata = null;
    Reader reader = null;

    try {
      reader = ReaderFactory.newXmlReader(file);

      metadata = mappingReader.read(reader, false);
    } catch(Exception ex) {
      log.warn("reading metadata from " + file.getPath(), ex);
      return;
    } finally {
      IOUtil.close(reader);
    }

    if (metadata.getVersioning().getSnapshot() != null) {
      if (log.isInfoEnabled()) {
        log.info("Skipping snapshot in " + file.getParent());
      }
      return;
    }
    
    if (!ctx.repoConfig.getLayout().equals("default")) {
      log.info("Skipping: Can only handle default repository layout");
      return;
    }
    
    // doesn't always return right result
    //String latestVersion = metadata.getVersioning().getLatest();
    String latestVersion = findLatestVersion(metadata.getVersioning());
    
    File artifactPath = new File(file.getParentFile(), latestVersion);
    File artifactFile = new File(artifactPath, String.format("%s-%s.war", metadata.getArtifactId(), latestVersion));
    processArtifact(artifactFile);
  }

  @Override
  public void processFile(String path) throws ConsumerException {
    try {
      processFile(path, true);
    } catch(Exception e) {
      throw new ConsumerException("processFile", e);
    }
  }

  @Override
  public void completeScan(boolean executeOnEntireRepo) {
    RepoContext ctx = getCurrentRepoContext();
    if (ctx == null) {
      return;
    }
    
    try {
      if (ctx.modified) {
        writeRepositoryInfo();
      }
    } catch(IOException e) {
      log.error("Writing Repository file", e);
    }
    log.trace("scan complete");
    resetProcessing();
    resetCurrentRepoContext();
  }

  @Override
  public void completeScan() {
    completeScan(true);
  }
 
 
  @Override
  public List<String> getExcludes() {
    return null;
  }

  @Override
  public List<String> getIncludes() {
    return this.includes;
  }

  @SuppressWarnings("unchecked")
  private String findLatestVersion(Versioning versioning) {
    LinkedList<String> versions = new LinkedList<String>();
    versions.addAll(versioning.getVersions());
    Collections.sort(versions, VersionComparator.getInstance());
    return versions.getLast();
  }

  private void writeRepositoryInfo() throws IOException {
  
    RepoContext ctx = getCurrentRepoContext();
    if (ctx == null) {
      throw new IllegalStateException("no context");
    }
    
    File repoFile = File.createTempFile(ctx.repoConfig.getId(), ".xml");
    FileWriter writer = new FileWriter(repoFile, false);
    try {
      XmlHelper.writeNode(ctx.packageDom, writer);
    } catch(TransformerException e) {
      log.error("writing DOM", e);
    } finally {
      IOUtil.close(writer);
    }
    File realRepoFile = new File(ctx.repositoryPath, repositoryXmlFileName);
    FileUtils.copyFile(repoFile, realRepoFile);
    repoFile.delete();
  }

  private void processArtifact(File file) {
    String path = file.getPath();
    if (!file.canRead()) {
      log.info("Cannot read " + path);
      return;
    }

    try {
      LiferayPluginPackage lrPackage = readPackageDescription(file);
      mergePackage(lrPackage);
    } catch(Exception e) {
      log.error("Reading Package description from: " + path, e);
    }
  }

  private void mergePackage(LiferayPluginPackage lrPackage) {
    RepoContext ctx = getCurrentRepoContext();
    if (ctx == null || lrPackage == null) {
      return;
    }

    String packageId = extractPackageId(lrPackage.getModuleId());
    String expr = String.format("//plugin-repository/plugin-package[starts-with(module-id,'%s')]", packageId);
    try {
      XPathFactory xpFactory = XPathFactory.newInstance();
      XPath xp = xpFactory.newXPath();
      Node node = (Node)xp.evaluate(expr, ctx.packageDom.getDocumentElement(), XPathConstants.NODE);
      boolean isNewPackage = false;
      if (node != null) {
        String oldModuleId = xpFactory.newXPath().evaluate("module-id", node);
        String newModuleId = lrPackage.getModuleId();
        if (!oldModuleId.equals(newModuleId)) {
          Node parentNode = node.getParentNode();
          parentNode.removeChild(node);
          isNewPackage = true;
        }
      } else {
        isNewPackage = true;
      }
      if (isNewPackage) {
        Element newNode = lrPackage.toNode(ctx.packageDom);
        ctx.packageDom.getDocumentElement().appendChild(newNode);
        ctx.modified = true;
      }
    } catch(Exception e) {
      log.info("merge package", e);
    }
  }

  /**
   * Extract groupId and artifactId from the given string. 
   * @param moduleId - in the form groupId/artifactId/version/packaging
   * @return groupId/artifactId
   */
  private String extractPackageId(String moduleId) {
    if (moduleId == null) {
      throw new IllegalArgumentException();
    }
    int idx = moduleId.indexOf('/');
    if (idx == -1) {
      throw new IllegalArgumentException(moduleId);
    }
    idx = moduleId.indexOf('/', idx + 1);
    if (idx == -1) {
      throw new IllegalArgumentException(moduleId);
    }
    
    return moduleId.substring(0, idx);
  }

  private boolean canStartProcessing() {
    long thId = Thread.currentThread().getId(); 
    return processingThreadId.compareAndSet(-1, thId);
  }
  
  private boolean isProcessing() {
    long thId = Thread.currentThread().getId();
    return processingThreadId.compareAndSet(thId, thId);
  }
  
  private void resetProcessing() {
    long thId = Thread.currentThread().getId();
    processingThreadId.compareAndSet(thId, -1);
  }
  
  private LiferayPluginPackage readPackageDescription(File file) throws Exception {
    ZipFile warFile = null;
    String path = file.getPath();
    if (log.isTraceEnabled()) {
      log.trace("reading package description from " + path);
    }
    try {
      warFile = new ZipFile(file);
      ZipEntry metaFile = warFile.getEntry("WEB-INF/liferay-plugin-package.xml");
      if (metaFile != null) {
        InputStream entryStream = warFile.getInputStream(metaFile);
        Reader inReader = new InputStreamReader(entryStream);
        
        Document packDoc = XmlHelper.createDocument(new InputSource(inReader));
        
        return LiferayPluginPackage.readXml(packDoc.getDocumentElement());
      }
      metaFile = warFile.getEntry("WEB-INF/liferay-plugin-package.properties");
      if (metaFile != null) {
        InputStream entryStream = warFile.getInputStream(metaFile);
        Properties props = new Properties();
        props.load(entryStream);
        LiferayPluginPackage lrPackage = LiferayPluginPackage.readProperties(props);
        return lrPackage;
      } 

    } finally {
      if (warFile != null) {
        warFile.close();
      }
    }
    log.info("no package description found in "+ path);
    return null;
  }

  private void readInitialDom(File file) {
    RepoContext ctx = getCurrentRepoContext();
    if (ctx.packageDom != null) {
      return;
    }
    FileInputStream is = null;
    InputSource src = null;
    File oldRepoFile = new File(file, repositoryXmlFileName);
    try {
      if (oldRepoFile.canRead()) {
        is = new FileInputStream(oldRepoFile);
        src = new InputSource(is);
      } else {
        Reader r = new StringReader("<plugin-repository></plugin-repository>");
        src = new InputSource(r);
        ctx.modified = true;
      }
      ctx.packageDom = XmlHelper.createDocument(src);
      
    } catch(Exception e) {
      log.error("creating document", e);
    } finally {
      IOUtil.close(is);
    }
  }

  @Override
  public String getDescription() {
    return this.description;
  }

  @Override
  public String getId() {
    return this.id;
  }

  @Override
  public boolean isPermanent() {
    return false;
  }

  @Override
  public boolean isProcessUnmodified() {
    return true;
  }

  @Override
  public void initialize() throws InitializationException {
    InputStream propStream = getClass().getResourceAsStream("/liferay-repository.properties");
    java.util.Properties p = new java.util.Properties();
    try {
      p.load(propStream);
    } catch(Exception e) {
      throw new InitializationException("Error Reading config file", e);
    }
    for (Map.Entry<Object, Object> propEntry : p.entrySet()) {
      String propKey = String.valueOf(propEntry.getKey());
      String propVal = String.valueOf(propEntry.getValue());
      if (propKey.startsWith("include")) {
        includes.add(propVal);
      }
    }
    liferayVersion = p.getProperty("lp.version", LiferayPluginPackage.DEFAULT_LIFERAY_VERSION);
    repositoryXmlFileName = String.format("liferay-plugin-repository-%s.xml", liferayVersion); 
  }

}
